feat(cli,api): difyctl use workspace + member management#36360
Open
lin-snow wants to merge 5 commits into
Open
feat(cli,api): difyctl use workspace + member management#36360lin-snow wants to merge 5 commits into
lin-snow wants to merge 5 commits into
Conversation
Contributor
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-05-20 09:17:15.213255037 +0000
+++ /tmp/pyrefly_pr.txt 2026-05-20 09:17:05.389187421 +0000
@@ -2182,6 +2182,26 @@
--> tests/unit_tests/controllers/openapi/test_workspaces.py:49:12
ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
--> tests/unit_tests/controllers/openapi/test_workspaces.py:50:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:114:12
+ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:115:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:120:12
+ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:121:12
+ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:122:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:127:12
+ERROR `in` is not supported between `Literal['DELETE']` and `None` [not-iterable]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:128:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:133:12
+ERROR `in` is not supported between `Literal['PUT']` and `None` [not-iterable]
+ --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:134:12
+ERROR Object of class `NoneType` has no attribute `json`
+ERROR Object of class `NoneType` has no attribute `json`
ERROR Cannot index into `Iterable[bytes]` [bad-index]
--> tests/unit_tests/controllers/service_api/app/test_audio.py:190:16
ERROR Cannot index into `Response` [bad-index]
|
Contributor
Pyrefly Type Coverage
|
508e575 to
0a425bc
Compare
…ted openapi Adds five bearer-authed endpoints under /openapi/v1/workspaces/<id>/ (switch, members CRUD, role update) gated by a new @require_workspace_role decorator that returns 404 for non-members (matching the existing GET /workspaces/<id> convention so workspace IDs don't leak across tenants) and 403 for insufficient role. TenantService / RegisterService domain logic is reused as-is — invites still go through invite_new_member so the Celery activation email fires for newly-invited addresses. Owner is intentionally not assignable through invite or role-update; ownership transfer remains console-only. CLI gains five commands: difyctl use workspace <id> difyctl get member [-w <id>] [-o ...] difyctl create member --email <e> --role <r> [-w <id>] difyctl delete member <member-id> [-w <id>] difyctl set member <member-id> --role <r> [-w <id>] use workspace strictly orders POST /switch -> GET /workspaces -> saveHosts; any failure aborts with no local mutation so hosts.yml never diverges from the server. get member marks the calling account row with '*' (matched via hosts.yml bundle.account.id). --role is client-enum-validated to normal|admin before any HTTP call. The old `difyctl auth use` (a pure-local workspace picker) is removed — its semantics conflict with server-side switch and keeping it would only confuse. The "no workspace selected" hint now points at `difyctl use workspace <id>`.
Inline checks on POST /openapi/v1/workspaces/<id>/members for:
- SaaS subscription members.limit (members.limit_exceeded)
- EE license workspace_members cap (workspace_members.license_exceeded)
Envelope {code, message, hint} on the wire body so CLI error-mapper
can surface structured remediation guidance without edition awareness.
EE per-workspace allow_member_invite policy continues via service-layer
check_workspace_member_invite_permission inside invite_new_member.
Reruns pnpm gen-api-contract and pnpm tree:gen after rebasing onto upstream/feat/cli (which migrated CLI types to @dify/contracts). Adds the Member* types to the shared contract package and registers the new CLI commands (use workspace, create/delete/get/set member) in the build-time command tree.
…Workspace + simplify _member_response - invite_url is always set server-side (always-non-null URL build path); drop the misleading Optional so generated CLI/SDK types stop forcing callers through pointless null checks. - use/workspace: pickWorkspace was used in one of two adjacent shape conversions; inline both for symmetry. - _member_response: TenantAccountRole and AccountStatus are StrEnums — the getattr + `if role else ""` defenses are unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
create member -o json now surfaces the full MemberInviteResponse —
including invite_url, previously unreachable from the CLI (scripts
had to rely on the Celery activation email). set/delete return a
synthesized {id, role} / {id, deleted: true} payload; the server's
200 is the proof the mutation took, so no extra round-trip and no
race on concurrent role flips.
Each command grew a small *Output class implementing the framework's
FormattedPrintable (text/json) + NamePrintable. run.ts builds it
(colored success line precomputed); index.ts wraps in formatted()
and lets the runner emit. Mirrors get member's existing
table()-envelope pattern. No backend changes, no spec changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fa3c1d6 to
f5ad3b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Fixes #<issue number>.Summary
Adds server-side workspace switching and full member management to
difyctl, mirroring the console's/workspaces/current/memberssurface but bearer-authed withworkspace_idon the path.Backend — five new
/openapi/v1/endpoints, gated by@accept_subjects(ACCOUNT)+@require_workspace_role(...):/workspaces/<id>/switch/workspaces/<id>/members/workspaces/<id>/members/workspaces/<id>/members/<member_id>/role/workspaces/<id>/members/<member_id>require_workspace_rolereturns 404 for non-members (matchingGET /workspaces/<id>so workspace ids don't leak across tenants) and 403 for insufficient role. Owner is intentionally not assignable via invite or role-update — ownership transfer stays console-only. Domain logic is reused fromTenantService/RegisterServiceas-is; invites still go throughinvite_new_memberso the Celery activation email fires.Edition-aware quota gate on
POST /members: SaaS subscription cap →members.limit_exceeded, EE license cap →workspace_members.license_exceeded. Both return 403 with{code, message, hint}envelope; CE no-ops becauseFeatureServiceleaves both flags false unless billing/EE flips them.CLI — five commands;
-o {json,yaml,name,text}on every read and write:use workspacestrictly ordersPOST /switch → GET /workspaces → saveHosts; any failure aborts with no local mutation sohosts.ymlnever diverges.get memberflags the caller's row with*.--roleis client-enum-validated tonormal | adminbefore any HTTP call.create -o jsonexposes theinvite_urlso scripts can dispatch activation links out-of-band without relying on the Celery email.The old
difyctl auth use(pure-local workspace picker) is deleted — its semantics conflict with server-side switch and would only confuse. The "no workspace selected" hint now points atdifyctl use workspace <id>.Tests cover the role-gate decorator, every new endpoint + payload validator, both API clients, and each CLI command runner (incl. the no-fallback-on-switch-failure invariant + the new printer envelopes).
Screenshots
N/A — CLI-only.
Checklist
make lint && make type-check(backend) andcd web && pnpm exec vp staged(frontend) to appease the lint gods